//
//  ConfigSDKSettingsVC.swift
//  Hermes
//
//  Created by FH on 2021/6/25.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

class ConfigSDKSettingsVC : FLViewController, ScriptEngineDispatch, PropsStatesContext {
    let appContext = HermesContext()
    fileprivate static let states = StatesStore()
    private let localStates = ConfigSDKSettingsVC.states
    private let settings = AgoraSDKSettings()
    // views
    let btnTestLastMile = NSButton()
    let btnTestEcho = NSButton()
    let btnDevices = NSButton()
    let btnSaveSettings = NSButton()
    let btnResetStates = NSButton()
    let lblTestMessage = FLLabel()
    let divConfigDevices = NSView()
    let progressVolume = NSLevelIndicator()
    let cameraTestView = NSView()
    let scrollView = NSScrollView()
    let contentView = NSView()
    // inner states
    var currSDKInstance: AgoraRtcEngineKit? = nil
    var inLastMileTest = false
    var inEchoTest = false
    var inConfigDevices = false
    var isDeviceCameraputTesting = false
    var isDeviceOutputTesting = false
    var isDeviceInputTesting = false
    var deviceInfos = [AgoraRtcDeviceInfo]()
    
    override func setWindowStyle(currWindow: NSWindow) {
        currWindow.styleMask.remove(.fullSizeContentView)
        currWindow.styleMask.remove(.resizable)
        currWindow.title = "Config SDK Settings"
        currWindow.isMovableByWindowBackground = true
        let size = self.view.bounds.size
        currWindow.setContentSize(CGSize(width: size.width, height: size.height))
        currWindow.center()
        self.dataBinding()
    }
    
    override func setViewStyle() {
        let height: CGFloat = 780
        self.view.frame = CGRect(x: 0, y: 0, width: 800, height: height)
        // init @PropState and load data from appContext
        let appSettings = self.appContext.settings.toDictionary()
        self.bindStates(initState: ("settings", appSettings))
    }
    
    // REMARK: delegates for Test
    
    func createEngineForTest() {
        if let sdk = self.appContext.agoraSDK {
            self.currSDKInstance = sdk
        }
        let appId = AppAppIdType.staticCommunication.rawValue
        if let instance = self.appContext.engine.createInstance(nil, true,
                                                                "sharedEngineWithAppId:delegate:", [appId, ()]) as? AgoraRtcEngineKit {
            self.appContext.engine.running(with: instance)
        }
    }
    
    func stopEngineForTest() {
        if let sdk = self.currSDKInstance {
            self.appContext.engine.stop()
            self.appContext.engine.running(with: sdk)
            self.currSDKInstance = nil
        }
    }

    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], isSDKInstance: Bool, instanceClass: String) {
        if selector == "rtcEngine:reportAudioVolumeIndicationOfSpeakers:totalVolume:" {
            if let volume = argValues["totalVolume"] as? Int {
                self.progressVolume.integerValue = volume
            }
        } else if selector == "rtcEngine:lastmileQuality:" {
            if let quality = argValues["quality"] as? UInt {
                let desc = self.appContext.engine.getEnumDescription(quality, enumType: "AgoraNetworkQuality")
                self.lblTestMessage.flText = desc
            }
        } else {
            if self.inEchoTest {
                if selector == "rtcEngineConnectionDidInterrupted:" {
                    self.lblTestMessage.flText = "Connection Interrupted"
                } else if selector == "rtcEngineConnectionDidLost:" {
                    self.lblTestMessage.flText = "Connection Lost"
                } else if selector == "rtcEngine:didOccurError:" {
                    let errorCode = argValues["errorCode"] as! Int
                    let desc = self.appContext.engine.getEnumDescription(errorCode, enumType: "AgoraErrorCode")
                    self.lblTestMessage.flText = "Error: \(desc)"
                }
                if selector == "rtcEngineMediaEngineDidStartCall:" {
                    self.lblTestMessage.flText = "Media engine did start call"
                }
            }
        }
    }
    
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], isSDKInstance: Bool, instanceClass: String) {
        if selector == "successBlockWithSender:NSString:NSUInteger:NSInteger:" {
            if let channel = argValues["#0"] as? String, let uid = argValues["#1"] as? UInt, let elapsed = argValues["#2"] as? Int {
                let preMessage = self.lblTestMessage.flText
                self.lblTestMessage.flText = "\(preMessage)\nDid start echo test: channel \(channel), uid \(uid), elapsed \(elapsed)"
            }
        }
    }
    
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], returnValue: Any, isSDKInstance: Bool, instanceClass: String) { }

    // REMARK: layout UI
    
    private func dataBinding() {
        self.appContext.engine.addTarget(target: self, runInMainQueue: true)
        self.rac_appearState.subscribeNext {[weak self] val in
            if let self = self, !val {
                if self.inLastMileTest {
                    self.btnTestLastMile.sendAction()
                } else if self.inEchoTest {
                    self.btnTestEcho.sendAction()
                } else if self.inConfigDevices {
                    self.btnDevices.sendAction()
                }
                self.stopEngineForTest()
                self.appContext.engine.removeTarget(target: self)
            }
        }
        self.btnTestEcho.rac_eventTouch.subscribeNext {[weak self] btn in
            if let self = self {
                if self.inLastMileTest {
                    FLShowCloseAlert("please stop lastmile test first.")
                    self.btnTestEcho.flTitle = self.btnTestEcho.flTitle.switchButtonTitle()
                    return
                } else if self.inConfigDevices {
                    FLShowCloseAlert("please stop config devices first.")
                    self.btnTestEcho.flTitle = self.btnTestEcho.flTitle.switchButtonTitle()
                    return
                }
                let node = self.yoga.getNode(by: self.lblTestMessage)
                let waitStop = btn.flTitle.trimWhiteSpaces() == "Stop Test"
                if waitStop {
                    btn.flTitle = "Ehco Test"
                    node?.display = .none
                    self.inEchoTest = false
                    self.appContext.engine.invoke("stopEchoTest")
                    self.stopEngineForTest()
                } else {
                    btn.flTitle = "Stop Test"
                    node?.display = .flex
                    self.lblTestMessage.flText = "Echo Testing ..."
                    self.inEchoTest = true
                    self.createEngineForTest()
                    self.appContext.agoraSDK!.startEchoTest(withInterval: 10, successBlock: nil)
                    self.appContext.engine.invoke("startEchoTestWithInterval:successBlock:", 10, ())
                }

                self.yoga.apply()
            }
        }
        self.btnTestLastMile.rac_eventTouch.subscribeNext {[weak self] btn in
            if let self = self {
                if self.inEchoTest {
                    FLShowCloseAlert("please stop echo test first.")
                    self.btnTestLastMile.flTitle = self.btnTestLastMile.flTitle.switchButtonTitle()
                    return
                } else if self.inConfigDevices {
                    FLShowCloseAlert("please stop config devices first.")
                    self.btnTestLastMile.flTitle = self.btnTestLastMile.flTitle.switchButtonTitle()
                    return
                }
                let node = self.yoga.getNode(by: self.lblTestMessage)
                let waitStop = btn.flTitle.trimWhiteSpaces() == "Stop Test"
                if waitStop {
                    btn.flTitle = "LastMile Test"
                    node?.display = .none
                    self.lblTestMessage.flText = ""
                    self.inLastMileTest = false
                    self.appContext.engine.invoke("disableLastmileTest")
                    self.stopEngineForTest()
                } else {
                    btn.flTitle = "Stop Test"
                    node?.display = .flex
                    self.lblTestMessage.flText = "Last Mile Testing ..."
                    self.inLastMileTest = true
                    self.createEngineForTest()
                    self.appContext.engine.invoke("enableLastmileTest")
                }

                self.yoga.apply()
            }
        }
        self.btnDevices.rac_eventTouch.subscribeNext {[weak self] btn in
            if let self = self {
                if self.inEchoTest {
                    FLShowCloseAlert("please stop echo test first.")
                    self.btnDevices.flTitle = self.btnDevices.flTitle.switchButtonTitle()
                    return
                } else if self.inLastMileTest {
                    FLShowCloseAlert("please stop lastmile test first.")
                    self.btnDevices.flTitle = self.btnDevices.flTitle.switchButtonTitle()
                    return
                }
                let waitStop = btn.flTitle.trimWhiteSpaces() == "Stop Config"
                if waitStop {
                    btn.flTitle = "Devices Config"
                    self.inConfigDevices = false
                    self.layoutTestDevicesDiv(runTest: false, sdk: self.appContext.agoraSDK!)
                    self.stopDevicesTest()
                    self.stopEngineForTest()
                } else {
                    btn.flTitle = "Stop Config"
                    self.inConfigDevices = true
                    self.createEngineForTest()
                    self.layoutTestDevicesDiv(runTest: true, sdk: self.appContext.agoraSDK!)
                }

                self.yoga.apply()
            }
        }
        self.btnResetStates.rac_eventTouch.subscribeNext {[weak self] btn in
            if let self = self {
                self.settings.resetStates(forUpdate: true, self.localStates, self.appContext.settingsByScript ?? [:])
            }
        }
        self.btnSaveSettings.rac_eventTouch.subscribeNext {[weak self] btn in
            if let self = self {
                if self.appContext.updateSettingsStatas(self.localStates.toDictionary(sectionName: "settings")) {
                    FLShowCloseAlert("setting data saved successfully!")
                    btn.flTitle = btn.flTitle.switchButtonTitle()
                }
            }
        }
    }
    
    private func stopDevicesTest() {
        if let sdk = self.appContext.agoraSDK {
            if self.isDeviceInputTesting {
                sdk.stopRecordingDeviceTest()
                self.isDeviceInputTesting = false
            } else if self.isDeviceOutputTesting {
                sdk.stopPlaybackDeviceTest()
                self.isDeviceOutputTesting = false
            } else if self.isDeviceCameraputTesting {
                sdk.stopCaptureDeviceTest()
                if !self.appContext.settings.asVoiceSDK {
                    sdk.disableVideo()
                }
                self.isDeviceCameraputTesting = false
            }
        }
    }
    
    override func layoutUI(needsLayout: Bool, args: [String : Any?]? = nil) {
        let header = NSView()
        header.bind(to: yoga, self.view) { it in
            it.height = 36
            it.direction = .row
            it.alignItems = .center
        }
        
        self.btnDevices.bind(to: yoga, header) { it in
            it.width = 90
            it.height = 26
            it.marginLeft = 4
        }
        self.btnDevices.flTitle = "Devices Config"
        self.btnDevices.flTitleColor = .white
        self.btnDevices.flBackgroundColor = .gray
        self.btnDevices.flCornerRadius = 4
        
        self.btnTestEcho.bind(to: yoga, header) { it in
            it.width = 90
            it.height = 26
            it.marginLeft = 4
        }
        self.btnTestEcho.flTitle = "Echo Test"
        self.btnTestEcho.flTitleColor = .white
        self.btnTestEcho.flBackgroundColor = .gray
        self.btnTestEcho.flCornerRadius = 4
        
        self.btnTestLastMile.bind(to: yoga, header) { it in
            it.width = 90
            it.height = 26
            it.marginLeft = 4
        }
        self.btnTestLastMile.flTitle = "LastMile Test"
        self.btnTestLastMile.flTitleColor = .white
        self.btnTestLastMile.flBackgroundColor = .gray
        self.btnTestLastMile.flCornerRadius = 4
        
        let rightOfHeader = NSView()
        rightOfHeader.bind(to: yoga, header) { it in
            it.height = 36
            it.direction = .rowReverse
            it.alignItems = .center
            it.position = .absolute
            it.right = 0
            it.top = 0
        }
        
        self.btnSaveSettings.bind(to: yoga, rightOfHeader) { it in
            it.width = 85
            it.height = 26
            it.marginRight = 4
        }
        self.btnSaveSettings.flTitle = "Save"
        self.btnSaveSettings.flTitleColor = .white
        self.btnSaveSettings.flBackgroundColor = .orange
        self.btnSaveSettings.flCornerRadius = 4
        
        self.btnResetStates.bind(to: yoga, rightOfHeader) { it in
            it.width = 90
            it.height = 26
            it.marginRight = 4
        }
        self.btnResetStates.flTitle = "Reset States"
        self.btnResetStates.flTitleColor = .white
        self.btnResetStates.flBackgroundColor = .gray
        self.btnResetStates.flCornerRadius = 4

        self.lblTestMessage.bind(to: yoga, self.view) { it in
            it.display = .none
            it.marginHorizontal = 4
            it.width = 792
            it.height = 60
        }
        self.lblTestMessage.enableMultipleLineMode()
        self.lblTestMessage.textColor = .black
        self.lblTestMessage.font = NSFont.systemFont(ofSize: 12)
        self.lblTestMessage.flBackgroundColor = .lightGray
        
        self.layoutTestDevicesDiv(runTest: false, sdk: nil)
        self.layoutScrollView()
        yoga.apply()
    }
    
    // REMARK: layout test devices
    private func layoutTestDevicesDiv(runTest: Bool, sdk: AgoraRtcEngineKit?) {
        if sdk == nil {
            self.divConfigDevices.bind(to: yoga, self.view) { it in
                it.display = .none
                it.marginHorizontal = 4
                it.height = 330
            }
        } else {
            let node = self.yoga.getNode(by: self.divConfigDevices)
            node?.display = .flex
        }
        self.divConfigDevices.flBackgroundColor = .lightGray
        if runTest, let sdk = sdk {
            let getDevicesFn: Action2<AgoraMediaDeviceType, NSPopUpButton> = {[weak self] (type, select) in
                if let self = self {
                    if type == .videoCapture && !self.appContext.settings.asVoiceSDK {
                        sdk.enableVideo()
                    }
                    if let devices = sdk.enumerateDevices(type), let currDevice = sdk.getDeviceInfo(type) {
                        self.deviceInfos.append(contentsOf: devices)
                        select.addItems(withTitles: devices.map({ $0.deviceName ?? "" }))
                        if let index = devices.firstIndex(where: { $0.deviceId == currDevice.deviceId }) {
                            select.selectItem(at: index)
                            select.rac_eventTouch.subscribeNext { ctrl in
                                if let info = self.deviceInfos.first(where: { $0.deviceName == ctrl.selectedItemTitle }) {
                                    if let currDeviceId = sdk.getDeviceInfo(type)?.deviceId, let selectId = info.deviceId,
                                       currDeviceId != selectId {
                                        sdk.setDevice(type, deviceId: info.deviceId!)
                                    }
                                }
                            }
                        }
                    }
                }
            }
            let selectorInputs = NSPopUpButton()
            getDevicesFn(.audioRecording, selectorInputs)
            let selectorOutputs = NSPopUpButton()
            getDevicesFn(.audioPlayout, selectorOutputs)
            let selectorCameraputs = NSPopUpButton()
            getDevicesFn(.videoCapture, selectorCameraputs)
            
            let line1 = NSView()
            line1.bind(to: yoga, self.divConfigDevices) { it in
                it.direction = .row
                it.height = 36
                it.alignItems = .center
            }
            let lblInput = FLLabel()
            lblInput.bind(to: yoga, line1) { it in
                it.width = 110
                it.height = 20
            }
            lblInput.textColor = .black
            lblInput.font = NSFont.systemFont(ofSize: 12)
            lblInput.flText = "Input Devices:"
            selectorInputs.bind(to: yoga, line1) { it in
                it.width = 280
                it.height = 30
            }
            let btnTestInput = NSButton()
            btnTestInput.bind(to: yoga, line1) { it in
                it.width = 90
                it.height = 26
                it.marginRight = 12
            }
            btnTestInput.flTitle = "Test"
            btnTestInput.flTitleColor = .white
            btnTestInput.flBackgroundColor = .gray
            btnTestInput.flCornerRadius = 4
            btnTestInput.rac_eventTouch.subscribeNext {[weak self] btn in
                if let self = self {
                    if self.isDeviceInputTesting {
                        btn.flTitle = "Test"
                        self.progressVolume.isHidden = true
                        self.stopDevicesTest()
                    } else {
                        self.isDeviceInputTesting = true
                        self.progressVolume.isHidden = false
                        btn.flTitle = "Stop Test"
                        sdk.startRecordingDeviceTest(200)
                    }
                }
            }

            let line2 = NSView()
            line2.bind(to: yoga, self.divConfigDevices) { it in
                it.direction = .row
                it.height = 36
                it.alignItems = .center
            }
            let lblOutput = FLLabel()
            lblOutput.bind(to: yoga, line2) { it in
                it.width = 110
                it.height = 20
            }
            lblOutput.textColor = .black
            lblOutput.font = NSFont.systemFont(ofSize: 12)
            lblOutput.flText = "Output Devices:"
            selectorOutputs.bind(to: yoga, line2) { it in
                it.width = 280
                it.height = 30
            }
            let btnTestOutput = NSButton()
            btnTestOutput.bind(to: yoga, line2) { it in
                it.width = 90
                it.height = 26
                it.marginRight = 12
            }
            btnTestOutput.flTitle = "Test"
            btnTestOutput.flTitleColor = .white
            btnTestOutput.flBackgroundColor = .gray
            btnTestOutput.flCornerRadius = 4
            btnTestOutput.rac_eventTouch.subscribeNext {[weak self] btn in
                if let self = self {
                    if self.isDeviceOutputTesting {
                        btn.flTitle = "Test"
                        self.stopDevicesTest()
                    } else {
                        self.isDeviceOutputTesting = true
                        btn.flTitle = "Stop Test"
                        if let path = Bundle.main.path(forResource: "test", ofType: "wav") {
                            sdk.startPlaybackDeviceTest(path)
                        }
                    }
                }
            }
            
            let line3 = NSView()
            line3.bind(to: yoga, self.divConfigDevices) { it in
                it.direction = .row
                it.height = 36
                it.alignItems = .center
            }
            let lblCamera = FLLabel()
            lblCamera.bind(to: yoga, line3) { it in
                it.width = 110
                it.height = 20
            }
            lblCamera.textColor = .black
            lblCamera.font = NSFont.systemFont(ofSize: 12)
            lblCamera.flText = "Camera Devices:"
            selectorCameraputs.bind(to: yoga, line3) { it in
                it.width = 280
                it.height = 30
            }
            let btnTestCamera = NSButton()
            btnTestCamera.bind(to: yoga, line3) { it in
                it.width = 90
                it.height = 26
                it.marginRight = 12
            }
            btnTestCamera.flTitle = "Test"
            btnTestCamera.flTitleColor = .white
            btnTestCamera.flBackgroundColor = .gray
            btnTestCamera.flCornerRadius = 4
            btnTestCamera.rac_eventTouch.subscribeNext {[weak self] btn in
                if let self = self {
                    if self.isDeviceCameraputTesting {
                        btn.flTitle = "Test"
                        self.stopDevicesTest()
                    } else {
                        self.isDeviceCameraputTesting = true
                        btn.flTitle = "Stop Test"
                        sdk.startCaptureDeviceTest(self.cameraTestView)
                    }
                }
            }
            
            self.progressVolume.bind(to: yoga, self.divConfigDevices) { it in
                it.height = 20
                it.width = 280
                it.marginLeft = 110
            }
            self.progressVolume.isHidden = true
            self.progressVolume.maxValue = 255
            
            self.cameraTestView.bind(to: yoga, self.divConfigDevices) { it in
                it.marginLeft = 110
                it.width = 280
                it.height = 192
            }
            self.cameraTestView.flBackgroundColor = .darkGray
        } else if sdk != nil && !runTest {
            for subView in self.divConfigDevices.subviews {
                yoga.removeNode(by: subView)
            }
            if let node = yoga.getNode(by: self.divConfigDevices) {
                node.display = .none
            }
        }
    }
    
    // REMARK: layout scrollView for states of settings
    private func layoutScrollView() {
        self.scrollView.bind(to: yoga, self.view) { it in
            it.marginHorizontal = 4
            it.height = (self.view.bounds.size.height - 36 - 8).toFLValue()
        }
        self.contentView.bind(to: yoga, self.view) { it in
            it.minHeight = (self.view.bounds.size.height - 36 - 8).toFLValue()
        }
        self.scrollView.contentView.documentView = self.contentView
        // general
        self.layoutGeneralSection()
        // video
        self.layoutVideoSection()
        // bundleSettings
        self.layoutBundleSettings()
    }
    
    private func layoutGeneralSection() {
        let section = NSView()
        section.bind(to: yoga, self.contentView) { it in
            it.paddingHorizontal = 4
        }
        let lblSection = FLLabel()
        lblSection.bind(to: yoga, section) { it in
            it.height = 20
            it.marginTop = 8
        }
        lblSection.textColor = .black
        lblSection.font = NSFont.boldSystemFont(ofSize: 14)
        lblSection.flText = "General Section:"
        // valid time
        let line1 = NSView()
        line1.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let lblValidAudioTime = FLLabel()
        lblValidAudioTime.bind(to: yoga, line1) { it in
            it.width = 90
            it.height = 20
        }
        lblValidAudioTime.textColor = .black
        lblValidAudioTime.font = NSFont.systemFont(ofSize: 12)
        lblValidAudioTime.flText = "ValidAudioTime:"
        let selectValidAudioTime = NSPopUpButton()
        selectValidAudioTime.bind(to: yoga, line1) { it in
            it.width = 100
            it.height = 26
        }
        selectValidAudioTime.addItems(withTitles: AppTokenValidTime.allCaseDescriptions)
        
        let lblValidVideoTime = FLLabel()
        lblValidVideoTime.bind(to: yoga, line1) { it in
            it.width = 90
            it.height = 20
            it.marginLeft = 2
        }
        lblValidVideoTime.textColor = .black
        lblValidVideoTime.font = NSFont.systemFont(ofSize: 12)
        lblValidVideoTime.flText = "ValidVideoTime:"
        let selectValidVideoTime = NSPopUpButton()
        selectValidVideoTime.bind(to: yoga, line1) { it in
            it.width = 100
            it.height = 26
        }
        selectValidVideoTime.addItems(withTitles: AppTokenValidTime.allCaseDescriptions)
        
        let lblValidDataTime = FLLabel()
        lblValidDataTime.bind(to: yoga, line1) { it in
            it.width = 90
            it.height = 20
            it.marginLeft = 2
        }
        lblValidDataTime.textColor = .black
        lblValidDataTime.font = NSFont.systemFont(ofSize: 12)
        lblValidDataTime.flText = "ValidDataTime:"
        let selectValidDataTime = NSPopUpButton()
        selectValidDataTime.bind(to: yoga, line1) { it in
            it.width = 100
            it.height = 26
        }
        selectValidDataTime.addItems(withTitles: AppTokenValidTime.allCaseDescriptions)
        
        let lblValidJoinTime = FLLabel()
        lblValidJoinTime.bind(to: yoga, line1) { it in
            it.width = 90
            it.height = 20
            it.marginLeft = 2
        }
        lblValidJoinTime.textColor = .black
        lblValidJoinTime.font = NSFont.systemFont(ofSize: 12)
        lblValidJoinTime.flText = "ValidJoinTime:"
        let selectValidJoinTime = NSPopUpButton()
        selectValidJoinTime.bind(to: yoga, line1) { it in
            it.width = 100
            it.height = 26
        }
        selectValidJoinTime.addItems(withTitles: AppTokenValidTime.allCaseDescriptions)
        // mediaIO
        let bufferTypeItems = getCaseItemDescriptions(by: "AgoraVideoBufferType", self.appContext.engine)
        let pixelFormatItems = getCaseItemDescriptions(by: "AgoraVideoPixelFormat", self.appContext.engine)
        let line2 = NSView()
        line2.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let chkVideoSource = FLCheckbox()
        chkVideoSource.bind(to: yoga, line2) { it in
            it.height = 20
            it.width = 180
        }
        chkVideoSource.font = NSFont.systemFont(ofSize: 12)
        chkVideoSource.flTitle = "MediaIO VideoSource"
        let selectVideoSourceBufferType = NSPopUpButton()
        selectVideoSourceBufferType.addItems(withTitles: bufferTypeItems)
        selectVideoSourceBufferType.bind(to: yoga, line2) { it in
            it.height = 26
            it.width = 140
        }
        let selectVideoSourcePixelFormat = NSPopUpButton()
        selectVideoSourcePixelFormat.addItems(withTitles: pixelFormatItems)
        selectVideoSourcePixelFormat.bind(to: yoga, line2) { it in
            it.height = 26
            it.width = 140
        }
        let line3 = NSView()
        line3.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let chkLocalRender = FLCheckbox()
        chkLocalRender.bind(to: yoga, line3) { it in
            it.height = 20
            it.width = 180
        }
        chkLocalRender.font = NSFont.systemFont(ofSize: 12)
        chkLocalRender.flTitle = "MediaIO LocalRender"
        let selectLocalRenderBufferType = NSPopUpButton()
        selectLocalRenderBufferType.addItems(withTitles: bufferTypeItems)
        selectLocalRenderBufferType.bind(to: yoga, line3) { it in
            it.height = 26
            it.width = 140
        }
        let selectLocalRenderPixelFormat = NSPopUpButton()
        selectLocalRenderPixelFormat.addItems(withTitles: pixelFormatItems)
        selectLocalRenderPixelFormat.bind(to: yoga, line3) { it in
            it.height = 26
            it.width = 140
        }
        let line4 = NSView()
        line4.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let chkRemoteRender = FLCheckbox()
        chkRemoteRender.bind(to: yoga, line4) { it in
            it.height = 20
            it.width = 180
        }
        chkRemoteRender.font = NSFont.systemFont(ofSize: 12)
        chkRemoteRender.flTitle = "MediaIO RemoteRender"
        let selectRemoteRenderBufferType = NSPopUpButton()
        selectRemoteRenderBufferType.addItems(withTitles: bufferTypeItems)
        selectRemoteRenderBufferType.bind(to: yoga, line4) { it in
            it.height = 26
            it.width = 140
        }
        let selectRemoteRenderPixelFormat = NSPopUpButton()
        selectRemoteRenderPixelFormat.addItems(withTitles: pixelFormatItems)
        selectRemoteRenderPixelFormat.bind(to: yoga, line4) { it in
            it.height = 26
            it.width = 140
        }
        let line5 = NSView()
        line5.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let chkAudioSource = FLCheckbox()
        chkAudioSource.bind(to: yoga, line5) { it in
            it.height = 20
            it.width = 180
        }
        chkAudioSource.font = NSFont.systemFont(ofSize: 12)
        chkAudioSource.flTitle = "External AudioSource"
        let lblSampleRate = FLLabel()
        lblSampleRate.bind(to: yoga, line5) { it in
            it.height = 20
        }
        lblSampleRate.textColor = .black
        lblSampleRate.font = NSFont.systemFont(ofSize: 12)
        lblSampleRate.flText = "SampleRate:"
        let inputSampleRate = FLInputField()
        inputSampleRate.bind(to: yoga, line5) { it in
            it.height = 24
            it.width = 100
            it.marginLeft = 4
        }
        let lblChannelPerFrame = FLLabel()
        lblChannelPerFrame.bind(to: yoga, line5) { it in
            it.height = 20
            it.marginLeft = 12
        }
        lblChannelPerFrame.textColor = .black
        lblChannelPerFrame.font = NSFont.systemFont(ofSize: 12)
        lblChannelPerFrame.flText = "Channel's PerFrame:"
        let inputChannelPerFrame = FLInputField()
        inputChannelPerFrame.bind(to: yoga, line5) { it in
            it.height = 24
            it.width = 100
            it.marginLeft = 4
        }
        // states to controls
        self.settings.$validAudioTime.bindTo(selectValidAudioTime)
        self.settings.$validVideoTime.bindTo(selectValidVideoTime)
        self.settings.$validDataTime.bindTo(selectValidDataTime)
        self.settings.$validJoinTime.bindTo(selectValidJoinTime)
        self.settings.$enableVideoSource.signal().subscribeNext { val in
            chkVideoSource.state = val ? .on : .off
            selectVideoSourceBufferType.isEnabled = val
            selectVideoSourcePixelFormat.isEnabled = val
        }
        self.settings.$enableLocalRender.signal().subscribeNext { val in
            chkLocalRender.state = val ? .on : .off
            selectLocalRenderBufferType.isEnabled = val
            selectLocalRenderPixelFormat.isEnabled = val
        }
        self.settings.$enableRemoteRender.signal().subscribeNext { val in
            chkRemoteRender.state = val ? .on : .off
            selectRemoteRenderBufferType.isEnabled = val
            selectRemoteRenderPixelFormat.isEnabled = val
        }
        self.settings.$enableAudioSource.signal().subscribeNext { val in
            chkAudioSource.state = val ? .on : .off
            inputSampleRate.isEnabled = val
            inputChannelPerFrame.isEnabled = val
        }
        // controls to states
        self.localStates.setState(self.appContext.engine!, byKey: "engine", sectionName: "")
        RACAssign(self.settings).validAudioTime = selectValidAudioTime.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).validVideoTime = selectValidVideoTime.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).validDataTime = selectValidDataTime.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).validJoinTime = selectValidJoinTime.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).enableVideoSource = chkVideoSource.rac_valueDidChanged
        RACAssign(self.settings).enableLocalRender = chkLocalRender.rac_valueDidChanged
        RACAssign(self.settings).enableRemoteRender = chkRemoteRender.rac_valueDidChanged
        RACAssign(self.settings).enableAudioSource = chkAudioSource.rac_valueDidChanged
        handleMediaIOAssign("mediaIOVideoSource", self.settings.mediaIOVideoSource,
                            selectVideoSourceBufferType, selectVideoSourcePixelFormat)
        handleMediaIOAssign("mediaIOLocalRender", self.settings.mediaIOLocalRender,
                            selectLocalRenderBufferType, selectLocalRenderPixelFormat)
        handleMediaIOAssign("mediaIORemoteRender", self.settings.mediaIORemoteRender,
                            selectRemoteRenderBufferType, selectRemoteRenderPixelFormat)
        inputSampleRate.flText = "\(self.settings.externalAudioSource.sampleRate)"
        inputChannelPerFrame.flText = "\(self.settings.externalAudioSource.channelsPerFrame)"
        inputSampleRate.rac_valueDidChanged.subscribeNext {[weak self] val in
            if let self = self {
                if let sampleRate = Int(val) {
                    let prop = self.settings.externalAudioSource
                    prop.sampleRate = sampleRate
                    self.settings.$externalAudioSource.update(value: prop, self.localStates)
                }
            }
        }
        inputChannelPerFrame.rac_valueDidChanged.subscribeNext {[weak self] val in
            if let self = self {
                if let frame = Int(val) {
                    let prop = self.settings.externalAudioSource
                    prop.channelsPerFrame = frame
                    self.settings.$externalAudioSource.update(value: prop, self.localStates)
                }
            }
        }
    }
    
    private func handleMediaIOAssign(_ propName: String, _ initValue: AppMediaIO,
                                     _ selectBuffer: NSPopUpButton, _ selectPixel: NSPopUpButton) {
        if let prop = self.localStates.getState(propName, sectionName: "settings") as? AppMediaIO {
            if let initBuffer = AgoraVideoBufferType(rawValue: initValue.bufferType) {
                selectBuffer.selectItem(at: initBuffer.getCurrIndex())
            }
            if let initPixel = AgoraVideoPixelFormat(rawValue: initValue.pixelFormat) {
                selectPixel.selectItem(at: initPixel.getCurrIndex())
            }
            selectBuffer.rac_eventTouch.subscribeNext {ctrl in
                if let buffer = AgoraVideoBufferType.getValue(by: ctrl.selectedComplexEnumIndex) as? Int {
                    prop.bufferType = buffer
                    self.localStates.updateState(prop, byKey: propName, sectionName: "settings")
                }
            }
            selectPixel.rac_eventTouch.subscribeNext {ctrl in
                if let pixel = AgoraVideoPixelFormat.getValue(by: ctrl.selectedComplexEnumIndex) as? UInt {
                    prop.pixelFormat = pixel
                    self.localStates.updateState(prop, byKey: propName, sectionName: "settings")
                }
            }
        }
    }
    
    private func layoutVideoSection() {
        let section = NSView()
        section.bind(to: yoga, self.contentView)
        // VideoEncoderConfiguration
        let line1 = NSView()
        line1.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let selectDimension = NSPopUpButton()
        selectDimension.addItems(withTitles: AppVideoDimension.allCaseDescriptions)
        selectDimension.bind(to: yoga, line1) { it in
            it.width = 140
            it.height = 30
        }
        let inputDimensionWidth = FLInputField()
        inputDimensionWidth.bind(to: yoga, line1) { it in
            it.height = 24
            it.width = 90
        }
        let inputDimensionHeight = FLInputField()
        inputDimensionHeight.bind(to: yoga, line1) { it in
            it.height = 24
            it.width = 90
            it.marginLeft = 2
        }
        
        let selectFrameRate = NSPopUpButton()
        selectFrameRate.addItems(withTitles: AppVideoFrameRate.allCaseDescriptions)
        selectFrameRate.bind(to: yoga, line1) { it in
            it.width = 140
            it.height = 30
            it.marginLeft = 4
        }
        let inputFrameRate = FLInputField()
        inputFrameRate.bind(to: yoga, line1) { it in
            it.height = 24
            it.width = 90
        }
        
        let selectBitrate = NSPopUpButton()
        selectBitrate.addItems(withTitles: AppVideoBitrate.allCaseDescriptions)
        selectBitrate.bind(to: yoga, line1) { it in
            it.width = 140
            it.height = 30
            it.marginLeft = 4
        }
        let inputBitrate = FLInputField()
        inputBitrate.bind(to: yoga, line1) { it in
            it.height = 24
            it.width = 90
        }
        // disable video
        let line2 = NSView()
        line2.bind(to: yoga, section) { it in
            it.height = 30
            it.direction = .row
            it.alignItems = .center
        }
        let chkVoiceSDK = FLCheckbox()
        chkVoiceSDK.bind(to: yoga, line2) { it in
            it.height = 20
            it.width = 120
        }
        chkVoiceSDK.font = NSFont.systemFont(ofSize: 12)
        chkVoiceSDK.flTitle = "As Voice SDK"
        chkVoiceSDK.flTitleColor = .orange
        
        let chkDisableVideo = FLCheckbox()
        chkDisableVideo.bind(to: yoga, line2) { it in
            it.height = 20
            it.width = 120
        }
        chkDisableVideo.font = NSFont.systemFont(ofSize: 12)
        chkDisableVideo.flTitle = "Disable Video"
        
        let chkDisableLocalPreview = FLCheckbox()
        chkDisableLocalPreview.bind(to: yoga, line2) { it in
            it.height = 20
            it.width = 145
        }
        chkDisableLocalPreview.font = NSFont.systemFont(ofSize: 12)
        chkDisableLocalPreview.flTitle = "Disable Local Preview"
        
        let lblVideoRenderMode = FLLabel()
        lblVideoRenderMode.bind(to: yoga, line2) { it in
            it.height = 20
            it.marginLeft = 58
        }
        lblVideoRenderMode.textColor = .black
        lblVideoRenderMode.font = NSFont.systemFont(ofSize: 12)
        lblVideoRenderMode.flText = "VideoRenderMode: "
        let selectRenderMode = NSPopUpButton()
        selectRenderMode.bind(to: yoga, line2) { it in
            it.height = 26
            it.width = 140
            it.marginLeft = 2
        }
        selectRenderMode.addItems(withTitles: getCaseItemDescriptions(by: "AgoraVideoRenderMode",
                                                                      self.appContext.engine))
        
        // split line
        let splitLine = NSView()
        splitLine.bind(to: yoga, section) { it in
            it.height = 0.5
            it.marginHorizontal = 8
            it.marginTop = 4
        }
        splitLine.flBackgroundColor = .darkGray
        // data binding
        handleCustomComplexEnum(self.settings.videoDimension, selectDimension, { (isCustomItem, val) in
            if let size = val as? CGSize {
                inputDimensionWidth.flText = "\(size.width)"
                inputDimensionHeight.flText = "\(size.height)"
                inputDimensionWidth.isEnabled = isCustomItem
                inputDimensionHeight.isEnabled = isCustomItem
            }
        }, {[weak self] val in self?.localStates.updateState(val, byKey: "videoDimension", sectionName: "settings")})
        handleCustomComplexEnum(self.settings.videoFrameRate, selectFrameRate, { (isCustomItem, val) in
            if let frameRate = val as? Int {
                inputFrameRate.flText = "\(frameRate)"
                inputFrameRate.isEnabled = isCustomItem
            }
        }, {[weak self] val in self?.localStates.updateState(val, byKey: "videoFrameRate", sectionName: "settings")})
        handleCustomComplexEnum(self.settings.videoBitrate, selectBitrate, { (isCustomItem, val) in
            if let bitrate = val as? Int {
                inputBitrate.flText = "\(bitrate)"
                inputBitrate.isEnabled = isCustomItem
            }
        }, {[weak self] val in self?.localStates.updateState(val, byKey: "videoBitrate", sectionName: "settings") })
        
        self.settings.$asVoiceSDK.bindTo(chkVoiceSDK)
        self.settings.$videoRenderMode.bindTo(selectRenderMode)
        self.settings.$shouldDisableVideo.bindTo(chkDisableVideo) { (chk, val) in
            if let boolVal = val, boolVal {
                chk.state = .on
            } else {
                chk.state = .off
            }
            chk.isEnabled = val != nil
        }
        self.settings.$shouldDisableLocalPreview.bindTo(chkDisableLocalPreview) { (chk, val) in
            if let boolVal = val, boolVal {
                chk.state = .on
            } else {
                chk.state = .off
            }
            chk.isEnabled = val != nil
        }
        chkVoiceSDK.rac_valueDidChanged.subscribeNext {[weak self] val in
            if let self = self {
                self.settings.$asVoiceSDK.update(value: val, self.localStates)
                if val {
                    self.settings.$shouldDisableVideo.update(value: nil, self.localStates)
                    self.settings.$shouldDisableLocalPreview.update(value: nil, self.localStates)
                } else {
                    self.settings.$shouldDisableVideo.update(value: false, self.localStates)
                    self.settings.$shouldDisableLocalPreview.update(value: false, self.localStates)
                }
            }
        }
        RACAssign(self.settings).shouldDisableVideo = chkDisableVideo.rac_valueDidChanged
        RACAssign(self.settings).shouldDisableLocalPreview = chkDisableLocalPreview.rac_valueDidChanged
        RACAssign(self.settings).videoRenderMode = selectRenderMode.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).videoDimension = selectDimension.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).videoFrameRate = selectFrameRate.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
        RACAssign(self.settings).videoBitrate = selectBitrate.rac_eventTouch.map({ $0.selectedComplexEnumIndex })
    }
    
    private func layoutBundleSettings() {
        let section = NSView()
        section.bind(to: yoga, self.contentView) { it in
            it.paddingHorizontal = 4
        }
        section.flBackgroundColor = .lightGray
        let lblSection = FLLabel()
        lblSection.bind(to: yoga, section) { it in
            it.height = 20
            it.marginTop = 8
        }
        lblSection.textColor = .black
        lblSection.font = NSFont.boldSystemFont(ofSize: 14)
        lblSection.flText = "Settings Bundle's Section:"
        // load from script's bundle settings
        let result = self.appContext.getCustomMappingControls(setStatesForSettings: false, filterSettings: true)
        for (keyPath, aliasName, _, mappingControl) in result {
            if let control = mappingControl, control.type != .none {
                let line = NSView()
                line.bind(to: yoga, section) { it in
                    it.paddingHorizontal = 4
                    it.height = 30
                    it.direction = .row
                    it.alignItems = .center
                }
                // checkbox
                if control.type == .checkbox {
                    let chk = FLCheckbox()
                    chk.bind(to: yoga, line) { it in
                        it.height = 20
                    }
                    chk.font = NSFont.systemFont(ofSize: 12)
                    chk.flTitle = aliasName
                    // data binding
                    statesBindTo(self.localStates, keyPath: keyPath, chk) {(ctrl, value) in
                        if let state = value as? Bool {
                            let newValue: NSControl.StateValue = state ? .on : .off
                            if ctrl.state != newValue {
                                ctrl.state = newValue
                            }
                        }
                    }
                    self.subscribe(keyPath: keyPath, chk, control)
                } else {
                    // label
                    let label = FLLabel()
                    label.bind(to: yoga, line) { it in
                        it.minWidth = 160
                        it.height = 20
                    }
                    label.textColor = .black
                    label.font = NSFont.systemFont(ofSize: 12)
                    label.flText = aliasName
                    // select
                    if control.type == .dropbox {
                        let select = NSPopUpButton()
                        select.bind(to: yoga, line) { it in
                            it.width = 160
                            it.height = 30
                        }
                        // data binding
                        var items: [String]! = nil
                        var selectIndex = 0
                        if let enumClass = control.enumClass {
                            items = getCaseItemDescriptions(by: enumClass, defaultItem: control.defaultSelectItem, self.appContext.engine)
                        } else if let strItems = control.stringItems {
                            items = Array(strItems)
                            if let defaultItem = control.defaultSelectItem {
                                items.insert(defaultItem, at: 0)
                            }
                        }
                        select.addItems(withTitles: items)
                        // bindTo control
                        statesBindTo(self.localStates, keyPath: keyPath, select) {[weak self] (ctrl, value) in
                            if let self = self {
                                if let enumClass = control.enumClass {
                                    selectIndex = selectIndexOfEnum(enumName:enumClass, value, self.appContext.engine)
                                    if control.defaultSelectItem != nil {
                                        selectIndex += 1
                                    }
                                    ctrl.selectItem(at: selectIndex)
                                } else if let initValue = value as? String,
                                          let index = ctrl.itemTitles.firstIndex(where: { $0 == initValue }) {
                                    ctrl.selectItem(at: index)
                                }
                            }
                        }
                        
                        self.subscribe(keyPath: keyPath, select, control)
                    } else if control.type == .inputField || control.type == .inputFile || control.type == .inputBinary {
                        // input
                        let input = FLInputField()
                        input.bind(to: yoga, line) { it in
                            it.width = 160
                            it.height = 24
                        }
                        // data binding
                        statesBindTo(self.localStates, keyPath: keyPath, input) {(ctrl, value) in
                            let (_, valueStr) = getAnyValueStr(from: value)
                            if input.flText != valueStr {
                                input.flText = valueStr
                            }
                        }
                        self.subscribe(keyPath: keyPath, input, control)
                    }
                }
            }
        }
    }
}

extension ConfigSDKSettingsVC {
    fileprivate func subscribe<ControlType: NSView>(keyPath: String, _ control: ControlType,
                                                    _ mappingControl: StateMappingControl) {
        statesSubscribeFrom(self.localStates, self.appContext.engine, keyPath: keyPath, control, mappingControl) {
            [weak self](value, keyPath) in
            if let self = self {
                if let value = value {
                    self.localStates.updateState(value as Any, byKey: keyPath, sectionName: "")
                } else {
                    self.localStates.updateState(value as Any, byKey: keyPath, sectionName: "")
                }
            }
        }
    }
}

extension RACAssign where Subject: AgoraSDKSettings {
    subscript<Property>(dynamicMember keyPath: KeyPath<Subject, Property>) -> IOMonad<Property> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = ConfigSDKSettingsVC.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { val in
                    localStates.updateState(val, byKey: propName, sectionName: "settings")
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
    
    subscript<Property>(dynamicMember keyPath: KeyPath<Subject, Property?>) -> IOMonad<Property> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = ConfigSDKSettingsVC.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { val in
                    localStates.updateState(val, byKey: propName, sectionName: "settings")
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
    
    subscript<Property: ComplexCaseIterable>(dynamicMember keyPath: KeyPath<Subject, Property>) -> IOMonad<ComplexEnumIndex> {
        get {
            fatalError("cannot read from this object.")
        }
        set(stream) {
            let localStates = ConfigSDKSettingsVC.states
            if let propName = findMatchedPropName(keyPath, self.subject.sectionName,
                                                  self.subject.toDictionary(),
                                                  localStates, subject: self.subject) {
                stream.take(until: self.subject.rac_willDealloc).subscribeNext { index in
                    if let enumItem = Property.self.getCaseItem(index) {
                        localStates.updateState(enumItem, byKey: propName, sectionName: "settings")
                    }
                }
            } else {
                fatalError("not found \(keyPath) mapping propName.")
            }
        }
    }
}
